package com.example.rxohosaudio;

import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.common.AudioProperty;
import ohos.media.common.Source;
import ohos.media.common.StorageProperty;
import ohos.media.recorder.Recorder;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@SuppressWarnings({"unused", "WeakerAccess", "UnusedReturnValue"})
public final class AudioRecorder {
    private static final String TAG = "AudioRecorder";
    private HiLogLabel label = new HiLogLabel(HiLog.LOG_APP, 0x00201, TAG);

    public static final int DEFAULT_SAMPLE_RATE = 44100;
    public static final int DEFAULT_BIT_RATE = 44100;
    public static final int ERROR_SDCARD_ACCESS = 1;
    public static final int ERROR_INTERNAL = 2;
    public static final int ERROR_NOT_PREPARED = 3;
    private static final int STOP_AUDIO_RECORD_DELAY_MILLIS = 300;
    private static final int RECORDER_TIME_UNIT = 1000;
    private static final int STATE_IDLE = 0;
    private static final int STATE_PREPARED = 1;
    private static final int STATE_RECORDING = 2;

    private int length = 0;

    private int mState = STATE_IDLE;
    private OnErrorListener mOnErrorListener;
    private long mSampleStart = 0;       // time at which latest record or play operation started
    private Recorder mRecorder;
    private boolean mStarted = false;

    private AudioRecorder() {
        // singleton
    }

    public static AudioRecorder getInstance() {
        return RxOhosAudioHolder.INSTANCE;
    }

    public void setOnErrorListener(final OnErrorListener listener) {
        mOnErrorListener = listener;
    }

    public synchronized int getMaxAmplitude() {
        if (mState != STATE_RECORDING) {
            return 0;
        }
        return mRecorder.obtainMaxAmplitude();
    }

    public int progress() {
        if (mState == STATE_RECORDING) {
            return (int) ((System.currentTimeMillis() - mSampleStart) / RECORDER_TIME_UNIT);
        }
        return 0;
    }

    /**
     * Directly start record, including prepare and start.
     * <p>
     * MediaRecorder.AudioSource.MIC
     * MediaRecorder.OutputFormat.MPEG_4
     * MediaRecorder.AudioEncoder.RAAC
     */
    //todo:@WorkerThread
    public boolean startRecord(final Source audioSource, final int outputFormat, AudioProperty audioProperty,
                                            final StorageProperty storageProperty) {
        //audioProperty 中 包含 sampleRate 和 bitRate 和 encoder
        //storageProperty 中 包含 输出文件位置
        stopRecord();
        mRecorder = new Recorder();
        mRecorder.setSource(audioSource);  // Source.AudioSource.SOUND_MIC
        mRecorder.setOutputFormat(outputFormat);
        mRecorder.setAudioProperty(audioProperty);
        mRecorder.setStorageProperty(storageProperty);

        // Handle IOException
//        new Thread(() -> {
            try {
                mRecorder.prepare();
            } catch (RuntimeException exception) {
                HiLog.warn(label, " startRecord fail, prepare fail:" + exception.getMessage());
                setError(ERROR_INTERNAL);
                mRecorder.reset();
                mRecorder.release();
                mRecorder = null;
                return false;
            }

            // Handle RuntimeException if the recording couldn't start
            try {
                mRecorder.start();
                mStarted = true;
            } catch (RuntimeException exception) {
                HiLog.warn(label, "startRecord fail, start fail:" + exception.getMessage());
                setError(ERROR_INTERNAL);
                mRecorder.reset();
                mRecorder.release();
                mRecorder = null;
                mStarted = false;
                return false;
            }
            mSampleStart = System.currentTimeMillis();
            mState = STATE_RECORDING;
//        });
        return true;
    }

    /**
     * prepare for a new audio record, with default sample rate and bit rate.
     */
    public boolean prepareRecordWithOutRate(final Source audioSource, final int outputFormat, final AudioProperty audioProperty,
                                                         final StorageProperty storageProperty) {
        return prepareRecord(audioSource, outputFormat, audioProperty, storageProperty);
    }

    /**
     * prepare for a new audio record.
     */
    public synchronized boolean prepareRecord(final Source audioSource, final int outputFormat, final AudioProperty audioProperty,
                                              final StorageProperty storageProperty) {
        stopRecord();

        mRecorder = new Recorder();
        mRecorder.setSource(audioSource);
        mRecorder.setOutputFormat(outputFormat);
        mRecorder.setAudioProperty(audioProperty);
        mRecorder.setStorageProperty(storageProperty);

        // Handle IOException
//        new Thread(() -> {
        try {
            boolean status = mRecorder.prepare();
            mState = STATE_PREPARED;
        } catch (Exception exception) {
            HiLog.error(label, " startRecord fail, prepare fail:" + exception.getMessage());
            setError(ERROR_INTERNAL);
            mRecorder.reset();
            mRecorder.release();
            mRecorder = null;
            return false;
        }
        mState = STATE_PREPARED;
        return true;
    }

    /**
     * After prepared, start record now.
     */
    public synchronized boolean startRecord() {
        if (mRecorder == null || mState != STATE_PREPARED) {
            setError(ERROR_NOT_PREPARED);
            return false;
        }
        // Handle RuntimeException if the recording couldn't start
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
        try {
            mRecorder.start();
            mStarted = true;
        } catch (RuntimeException exception) {
            HiLog.warn(label, " startRecord fail, start fail:" + exception.getMessage());
            setError(ERROR_INTERNAL);
            mRecorder.reset();
            mRecorder.release();
            mRecorder = null;
            mStarted = false;
            return false;
        }
        mSampleStart = System.currentTimeMillis();
        mState = STATE_RECORDING;
//            }
//        });
        return true;
    }

    /**
     * stop record, and save audio file.
     *
     * @return record audio length in seconds, -1 if not a successful record.
     */
    public int stopRecord() {

        if (mRecorder == null) {
            mState = STATE_IDLE;
            return -1;
        }
        int length = -1;
        switch (mState) {
            case STATE_RECORDING:
//                new Thread(() -> {
                try {
                    mRecorder.stop();
                    mStarted = false;
                    length = (int) ((System.currentTimeMillis() - mSampleStart) / RECORDER_TIME_UNIT);
                } catch (RuntimeException e) {
                    HiLog.warn(label, " stopRecord fail, stop fail(no audio data recorded):" + e.getMessage());
                } catch (Exception e) {
                    HiLog.warn(label, "stopRecord fail, stop fail(catch Exception): " + e.getMessage());
                }
//                });
                // fall down
            case STATE_PREPARED:
                // fall down
            case STATE_IDLE:
                // fall down
            default:
                HiLog.warn(label, "state not recording!");
                try {
                    mRecorder.reset();
                } catch (RuntimeException e) {
                    HiLog.warn(label, "stopRecord fail, reset fail " + e.getMessage());
                }
                mRecorder.release();
                mRecorder = null;
                mState = STATE_IDLE;
                break;
        }
        return length;
    }

    private void setError(final int error) {
        if (mOnErrorListener != null) {
            mOnErrorListener.onError(error);
        }
    }

    /* returns recorder is started or not */
    public boolean isStarted() {
        return mStarted;
    }

    @IntDef(value = { ERROR_SDCARD_ACCESS, ERROR_INTERNAL, ERROR_NOT_PREPARED })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Error {
    }

    public interface OnErrorListener {
        //   todo:     @WorkerThread
        void onError(int error);
    }

    private static class RxOhosAudioHolder {
        private static final AudioRecorder INSTANCE = new AudioRecorder();
    }
}
